/*
 * Copyright (c) 2003, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.largeobject;

import org.postgresql.core.BaseConnection;
import org.postgresql.fastpath.Fastpath;
import org.postgresql.fastpath.FastpathArg;
import org.postgresql.util.ByteStreamWriter;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.SQLException;

/**
 * This class provides the basic methods required to run the interface, plus a pair of methods that
 * provide InputStream and OutputStream classes for this object.
 *
 * <p>Normally, client code would use the getAsciiStream, getBinaryStream, or getUnicodeStream methods
 * in ResultSet, or setAsciiStream, setBinaryStream, or setUnicodeStream methods in
 * PreparedStatement to access Large Objects.</p>
 *
 * <p>However, sometimes lower level access to Large Objects are required, that are not supported by
 * the JDBC specification.</p>
 *
 * <p>Refer to org.postgresql.largeobject.LargeObjectManager on how to gain access to a Large Object,
 * or how to create one.</p>
 *
 * @see org.postgresql.largeobject.LargeObjectManager
 * @see java.sql.ResultSet#getAsciiStream
 * @see java.sql.ResultSet#getBinaryStream
 * @see java.sql.ResultSet#getUnicodeStream
 * @see java.sql.PreparedStatement#setAsciiStream
 * @see java.sql.PreparedStatement#setBinaryStream
 * @see java.sql.PreparedStatement#setUnicodeStream
 */
@SuppressWarnings("deprecation") // support for deprecated Fastpath API
public class LargeObject
    implements AutoCloseable {

  /**
   * Indicates a seek from the beginning of a file.
   */
  public static final int SEEK_SET = 0;

  /**
   * Indicates a seek from the current position.
   */
  public static final int SEEK_CUR = 1;

  /**
   * Indicates a seek from the end of a file.
   */
  public static final int SEEK_END = 2;

  private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

  private final Fastpath fp; // Fastpath API to use
  private final long oid; // OID of this object
  private final int mode; // read/write mode of this object
  private final int fd; // the descriptor of the open large object

  private @Nullable BlobOutputStream os; // The current output stream

  private boolean closed; // true when we are closed

  private @Nullable BaseConnection conn; // Only initialized when open a LOB with CommitOnClose
  private final boolean commitOnClose; // Only initialized when open a LOB with CommitOnClose

  /**
   * This opens a large object.
   *
   * <p>If the object does not exist, then an SQLException is thrown.</p>
   *
   * @param fp FastPath API for the connection to use
   * @param oid of the Large Object to open
   * @param mode Mode of opening the large object
   * @param conn the connection to the database used to access this LOB
   * @param commitOnClose commit the transaction when this LOB will be closed (defined in
   *        LargeObjectManager)
   * @throws SQLException if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObjectManager
   */
  protected LargeObject(Fastpath fp, long oid, int mode,
      @Nullable BaseConnection conn, boolean commitOnClose)
      throws SQLException {
    this.fp = fp;
    this.oid = oid;
    this.mode = mode;
    if (commitOnClose) {
      this.commitOnClose = true;
      this.conn = conn;
    } else {
      this.commitOnClose = false;
    }

    FastpathArg[] args = new FastpathArg[2];
    args[0] = Fastpath.createOIDArg(oid);
    args[1] = new FastpathArg(mode);
    this.fd = fp.getInteger("lo_open", args);
  }

  /**
   * This opens a large object.
   *
   * <p>If the object does not exist, then an SQLException is thrown.</p>
   *
   * @param fp FastPath API for the connection to use
   * @param oid of the Large Object to open
   * @param mode Mode of opening the large object (defined in LargeObjectManager)
   * @throws SQLException if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObjectManager
   */
  protected LargeObject(Fastpath fp, long oid, int mode) throws SQLException {
    this(fp, oid, mode, null, false);
  }

  public LargeObject copy() throws SQLException {
    return new LargeObject(fp, oid, mode);
  }

  /*
   * Release large object resources during garbage cleanup.
   *
   * This code used to call close() however that was problematic because the scope of the fd is a
   * transaction, thus if commit or rollback was called before garbage collection ran then the call
   * to close would error out with an invalid large object handle. So this method now does nothing
   * and lets the server handle cleanup when it ends the transaction.
   *
   * protected void finalize() throws SQLException { }
   */

  /**
   * @return the OID of this LargeObject
   * @deprecated As of 8.3, replaced by {@link #getLongOID()}
   */
  @Deprecated
  public int getOID() {
    return (int) oid;
  }

  /**
   * @return the OID of this LargeObject
   */
  public long getLongOID() {
    return oid;
  }

  /**
   * This method closes the object. You must not call methods in this object after this is called.
   *
   * @throws SQLException if a database-access error occurs.
   */
  @Override
  public void close() throws SQLException {
    if (!closed) {
      // flush any open output streams
      if (os != null) {
        try {
          // we can't call os.close() otherwise we go into an infinite loop!
          os.flush();
        } catch (IOException ioe) {
          throw new PSQLException("Exception flushing output stream", PSQLState.DATA_ERROR, ioe);
        } finally {
          os = null;
        }
      }

      // finally close
      FastpathArg[] args = new FastpathArg[1];
      args[0] = new FastpathArg(fd);
      fp.fastpath("lo_close", args); // true here as we dont care!!
      closed = true;
      BaseConnection conn = this.conn;
      if (this.commitOnClose && conn != null) {
        conn.commit();
      }
    }
  }

  /**
   * Reads some data from the object, and return as a byte[] array.
   *
   * @param len number of bytes to read
   * @return byte[] array containing data read
   * @throws SQLException if a database-access error occurs.
   */
  public byte[] read(int len) throws SQLException {
    // This is the original method, where the entire block (len bytes)
    // is retrieved in one go.
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    byte[] bytes = fp.getData("loread", args);
    if (bytes == null) {
      return EMPTY_BYTE_ARRAY;
    }
    return bytes;
  }

  /**
   * Reads some data from the object into an existing array.
   *
   * @param buf destination array
   * @param off offset within array
   * @param len number of bytes to read
   * @return the number of bytes actually read
   * @throws SQLException if a database-access error occurs.
   */
  public int read(byte[] buf, int off, int len) throws SQLException {
    byte[] b = read(len);
    if (b.length == 0) {
      return 0;
    }
    len = Math.min(len, b.length);
    System.arraycopy(b, 0, buf, off, len);
    return len;
  }

  /**
   * Writes an array to the object.
   *
   * @param buf array to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(byte[] buf) throws SQLException {
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(buf);
    fp.fastpath("lowrite", args);
  }

  /**
   * Writes some data from an array to the object.
   *
   * @param buf destination array
   * @param off offset within array
   * @param len number of bytes to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(byte[] buf, int off, int len) throws SQLException {
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(buf, off, len);
    fp.fastpath("lowrite", args);
  }

  /**
   * Writes some data from a given writer to the object.
   *
   * @param writer the source of the data to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(ByteStreamWriter writer) throws SQLException {
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = FastpathArg.of(writer);
    fp.fastpath("lowrite", args);
  }

  /**
   * Sets the current position within the object.
   *
   * <p>This is similar to the fseek() call in the standard C library. It allows you to have random
   * access to the large object.</p>
   *
   * @param pos position within object
   * @param ref Either SEEK_SET, SEEK_CUR or SEEK_END
   * @throws SQLException if a database-access error occurs.
   */
  public void seek(int pos, int ref) throws SQLException {
    FastpathArg[] args = new FastpathArg[3];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(pos);
    args[2] = new FastpathArg(ref);
    fp.fastpath("lo_lseek", args);
  }

  /**
   * Sets the current position within the object using 64-bit value (9.3+).
   *
   * @param pos position within object
   * @param ref Either SEEK_SET, SEEK_CUR or SEEK_END
   * @throws SQLException if a database-access error occurs.
   */
  public void seek64(long pos, int ref) throws SQLException {
    FastpathArg[] args = new FastpathArg[3];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(pos);
    args[2] = new FastpathArg(ref);
    fp.fastpath("lo_lseek64", args);
  }

  /**
   * Sets the current position within the object.
   *
   * <p>This is similar to the fseek() call in the standard C library. It allows you to have random
   * access to the large object.</p>
   *
   * @param pos position within object from beginning
   * @throws SQLException if a database-access error occurs.
   */
  public void seek(int pos) throws SQLException {
    seek(pos, SEEK_SET);
  }

  /**
   * @return the current position within the object
   * @throws SQLException if a database-access error occurs.
   */
  public int tell() throws SQLException {
    FastpathArg[] args = new FastpathArg[1];
    args[0] = new FastpathArg(fd);
    return fp.getInteger("lo_tell", args);
  }

  /**
   * @return the current position within the object
   * @throws SQLException if a database-access error occurs.
   */
  public long tell64() throws SQLException {
    FastpathArg[] args = new FastpathArg[1];
    args[0] = new FastpathArg(fd);
    return fp.getLong("lo_tell64", args);
  }

  /**
   * This method is inefficient, as the only way to find out the size of the object is to seek to
   * the end, record the current position, then return to the original position.
   *
   * <p>A better method will be found in the future.</p>
   *
   * @return the size of the large object
   * @throws SQLException if a database-access error occurs.
   */
  public int size() throws SQLException {
    int cp = tell();
    seek(0, SEEK_END);
    int sz = tell();
    seek(cp, SEEK_SET);
    return sz;
  }

  /**
   * See #size() for information about efficiency.
   *
   * @return the size of the large object
   * @throws SQLException if a database-access error occurs.
   */
  public long size64() throws SQLException {
    long cp = tell64();
    seek64(0, SEEK_END);
    long sz = tell64();
    seek64(cp, SEEK_SET);
    return sz;
  }

  /**
   * Truncates the large object to the given length in bytes. If the number of bytes is larger than
   * the current large object length, the large object will be filled with zero bytes. This method
   * does not modify the current file offset.
   *
   * @param len given length in bytes
   * @throws SQLException if something goes wrong
   */
  public void truncate(int len) throws SQLException {
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    fp.getInteger("lo_truncate", args);
  }

  /**
   * Truncates the large object to the given length in bytes. If the number of bytes is larger than
   * the current large object length, the large object will be filled with zero bytes. This method
   * does not modify the current file offset.
   *
   * @param len given length in bytes
   * @throws SQLException if something goes wrong
   */
  public void truncate64(long len) throws SQLException {
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    fp.getInteger("lo_truncate64", args);
  }

  /**
   * Returns an {@link InputStream} from this object.
   *
   * <p>This {@link InputStream} can then be used in any method that requires an InputStream.</p>
   *
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream() throws SQLException {
    return new BlobInputStream(this);
  }

  /**
   * Returns an {@link InputStream} from this object, that will limit the amount of data that is
   * visible.
   *
   * @param limit maximum number of bytes the resulting stream will serve
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream(long limit) throws SQLException {
    return new BlobInputStream(this, BlobInputStream.DEFAULT_MAX_BUFFER_SIZE, limit);
  }

  /**
   * Returns an {@link InputStream} from this object, that will limit the amount of data that is
   * visible.
   * Added mostly for testing
   *
   * @param bufferSize buffer size for the stream
   * @param limit maximum number of bytes the resulting stream will serve
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream(int bufferSize, long limit) throws SQLException {
    return new BlobInputStream(this, bufferSize, limit);
  }

  /**
   * Returns an {@link OutputStream} to this object.
   *
   * <p>This OutputStream can then be used in any method that requires an OutputStream.</p>
   *
   * @return {@link OutputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public OutputStream getOutputStream() throws SQLException {
    if (os == null) {
      os = new BlobOutputStream(this);
    }
    return os;
  }
}
